using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using System.Linq;

namespace GuavaData
{
    public class Unpivot : Transform
    {
        List<string> tables;                //name of i-th table
        List<string> attrColumns;           //name of attribute column for i-th table
        List<string> valColumns;            //name of value column for i-th table


        // Fields for run-time state
        List<Domain.Domain> attrDomains;     //domain of attribute column for i-th table
        List<Domain.Domain> valueDomains;    //domain of value column for i-th table

        //For Each table in the list maintain a list of key attributes
        Dictionary<string, List<string>> keyNames;             //key attributes of each table
        Dictionary<string, List<string>> colNames;              //contains only non key columns
        Dictionary<string, Domain.Domain> keyDomains;               // contains a mapping of keys to their domains
        Dictionary<string, List<Domain.Domain>> colDomains;

        /// <summary>
        /// unpivot operation for a list of tables
        /// </summary>
        /// <param name="t">a list of tables to unpivot</param>
        /// <param name="a">a list of attribute column names for each table</param>
        /// <param name="v">a list of value column names for each table</param>

        public Unpivot(List<string> t, List<string> a, List<string> v)
        {

            if (t.Count != a.Count)
                throw new TransformException("The number of tables in a pivot operation must be the same as the number of attribute columns.");
            if (v.Count != a.Count)
                throw new TransformException("The number of value columns in a pivot operation must be the same as the number of attribute columns.");
            this.tables = t;
            this.attrColumns = a;
            this.valColumns = v;
            this.attrDomains = new List<GuavaData.Domain.Domain>(t.Count);
            this.valueDomains = new List<GuavaData.Domain.Domain>(t.Count);
            this.keyNames = new Dictionary<string, List<string>>(t.Count);
            this.colNames = new Dictionary<string, List<string>>(t.Count);
            this.keyDomains = new Dictionary<string, Domain.Domain>();
            this.colDomains = new Dictionary<string, List<GuavaData.Domain.Domain>>();


        }

        public List<string> Tables
        {
            get { return tables; }
        }



        /// <summary>
        /// expand AddTable statement
        /// </summary>
        /// <param name="at">AddTable object that adds a table</param>
        /// <param name="c">Command object corresponding to AddTable statement</param>

        internal override void Expand(AddTable at, Command c)
        {
            if (!(this.tables.Contains(at.Table)))
                return;

            List<string> newCols = new List<string>(); //New columns which will be passed to new addtable object
            List<string> newKeys = new List<string>(); //New keys which will be passed tp new addtable object
            List<Domain.Domain> newDom = new List<GuavaData.Domain.Domain>();// a list of domains.The first entry in this list is the domain of 1st column 
            //2nd domain in this list is the domain of the second column
            List<string> colsminuskeys = new List<string>();
            int i = this.tables.IndexOf(at.Table);
            string t = this.tables[i];
            string a = this.attrColumns[i];
            string v = this.valColumns[i];
            // List of Columns for new table is keys and Attribute and Value
            newCols.AddRange(at.Keys);
            newCols.Add(a);
            newCols.Add(v);
            //List of keys for new table is keys and Attribute
            newKeys.AddRange(at.Keys);
            newKeys.Add(a);
            //Add keys and columns to a dictionary to be able to access keys(Ta) columns(Ta)
            this.keyNames.Add(at.Table, at.Keys);
            this.colNames.Add(at.Table, at.Columns);

            List<Domain.Domain> domains = new List<Domain.Domain>(
                from d in at.Domains
                select d.Clone);

            this.colDomains.Add(at.Table, domains);
            //create a list of non key columns and memorize it in dictionary
            foreach (String col in at.Columns)
            {
                //copy the columns
                colsminuskeys.Add(col);
            }


            //the domain attribute of this object is list of names of columns and the domain of value is domain of the columns
            // domain of any column which is not a key columm add that domain as the domin of value column

            List<string> str = new List<string>();
            List<string> temp_keynames = this.keyNames[at.Table];

            foreach (String columnofaddtable in at.Columns)
            {
                if (!temp_keynames.Contains(columnofaddtable))
                {
                    str.Add(columnofaddtable);
                }
            }

            Domain.Enumerated e = new GuavaData.Domain.Enumerated(str);
            attrDomains.Add(e);

            foreach (string s in at.Columns)
            {
                if (!temp_keynames.Contains(s))
                {
                    int index = at.Columns.IndexOf(s);
                    Domain.Domain dom = at.Domains[index];
                    this.valueDomains.Add(dom);
                    break;
                }
            }

            foreach (string s in temp_keynames)
            {
                int index = at.Columns.IndexOf(s);
                Domain.Domain keyDom = at.Domains[index];
                
                keyDomains.Add(s, keyDom);
                newDom.Add(keyDom);
            }

            newDom.Add(attrDomains[i]);
            newDom.Add(valueDomains[i]);

            AddTable atNew = new AddTable(at.Table, newCols, newDom, newKeys);
            c.NotifyChange(new Command(atNew));


        }
        /// <summary>
        /// expand AddColumn statement
        /// </summary>
        /// <param name="ac">AddColumn object that add a column to the table</param>
        /// <param name="c">Command object corresponding to AddColumn statement</param>

        internal override void Expand(AddColumn ac, Command c)
        {
            // check:this.table and addcolumn.table are same and the column you are trying to
            //add is not already a key column.After doing these two checks create a new add element 
            // object .Notify change to current transaction
            if (!(this.tables.Contains(ac.Table)))
                return;
            if (this.keyNames[ac.Table].Contains(ac.Column))
                throw new Exception("There is already a column by this name");
            int i = this.tables.IndexOf(ac.Table);
            AddElement aenew = new AddElement(this.tables[i], this.attrColumns[i], ac.Column);
            //notify
            c.NotifyChange(new Command(aenew));
        }


        /// <summary>
        /// expand AlterColumnRename statement
        /// </summary>
        /// <param name="acr">AlterColumnRename object that rename a column of the table</param>
        /// <param name="c">Command object corresponding to AlterColumnRename statement</param>

        internal override void Expand(AlterColumnRename acr, Command c)
        {
            //check for 2 things:this.table is same as rename table and the columns to 
            //rename is not a key column.After doing this replace the current operator with 
            //a new remove element and do notifiychange
            if (!(this.tables.Contains(acr.Table)))
                return;
            if (this.keyNames[acr.Table].Contains(acr.OldName))
                return;
            int i = this.tables.IndexOf(acr.Table);
            AlterElementRename arnew = new AlterElementRename(tables[i], attrColumns[i], acr.OldName, acr.NewName);
            c.NotifyChange(c, new Command(arnew));


        }

        internal override void Expand(DropColumn dc, Command c)
        {
            // same as above except that the new operator is drop element instead of 
            // add column

            if (!(this.tables.Contains(dc.Table)))
                return;
            if (this.keyNames[dc.Table].Contains(dc.Column))
                return;
            int i = this.tables.IndexOf(dc.Table);
            DropElement denew = new DropElement(tables[i], attrColumns[i], dc.Column);
            c.NotifyChange(c, new Command(denew));


        }

        /// <summary>
        /// expand AddElement statement
        /// </summary>
        /// <param name="ae">AddElement object that add an element to the domain of one column of the table</param>
        /// <param name="c">Command object corresponding to AddElement statement</param>

        internal override void Expand(AddElement ae, Command c)
        {
            if (!(this.tables.Contains(ae.Table)))
                return;
            if (this.keyNames[ae.Table].Contains(ae.Column))
                return;
            int i = this.tables.IndexOf(ae.Table);
            List<string> columns = new List<string>(colNames[ae.Table]);
            List<string> keys = keyNames[ae.Table];
            //get all non key columns in form of a list
            foreach (string key in keys)
            {
                columns.Remove(key);
            }

            /*now the columns list contains only non key columns.In the current transaction
            check if there exists an addelement for each of the element in this columns list
            */
            //get current transaction
            //  List<Command> command= c.container.Contents;
            Transaction t = c.container;
            List<Command> commandlist = t.Contents;
            foreach (Command command in commandlist)
            {
                Operator op = command.root;
                Type type = op.GetType();
                String opname = type.ToString();
                if (opname.Equals("AddElement"))
                {
                    AddElement aenew = (AddElement)op;
                    AddElement newae = new AddElement(ae.Table, this.valColumns[i], ae.Element);
                    c.NotifyChange(c, new Command(newae));
                    String col = aenew.Column;
                    columns.Remove(col);

                }
            }

            if (columns != null)
            {
                throw new Exception("Error-There Exists a columns with no corresponding add element");
            }


        }

        /// <summary>
        /// expand AlterElementRename statement
        /// </summary>
        /// <param name="aer">AlterElementRename object that rename an element in the domain of one column of the table</param>
        /// <param name="c">Command object corresponding to AlterElementRename statement</param>
        internal override void Expand(AlterElementRename re, Command c)
        {
            if (!(this.tables.Contains(re.Table)))
                return;
            if (this.keyNames[re.Table].Contains(re.Column))
                return;
            int i = this.tables.IndexOf(re.Table);
            List<string> columns = new List<string>(colNames[re.Table]);
            List<string> keys = keyNames[re.Table];

            //get all non key columns in form of a list
            foreach (string key in keys)
            {
                columns.Remove(key);
            }


            /*now the columns list contains only non key columns.In the current transaction
            check if there exists an addelement for each of the element in this columns list
            */
            //get current transaction

            Transaction t = c.container;
            List<Command> commandlist = t.Contents;
            foreach (Command command in commandlist)
            {
                Operator op = command.root;
                Type type = op.GetType();
                String opname = type.ToString();
                if (opname.Equals("AlterElementRename"))
                {
                    AlterElementRename renew = new AlterElementRename(re.Table, this.valColumns[i], re.OldName, re.NewName);
                    c.NotifyChange(c, new Command(renew));
                    AlterElementRename newre = (AlterElementRename)op;
                    String col = newre.Column;
                    columns.Remove(col);

                }
            }

            if (columns != null)
            {
                throw new Exception("Union compatability Problem");
            }
            else
            {
                AlterElementRename renew = new AlterElementRename(re.Table, this.valColumns[i], re.OldName, re.NewName);
                c.NotifyChange(c, new Command(renew));

            }

        }


        /// <summary>
        /// expand DropElement statement
        /// </summary>
        /// <param name="de">DropElement object that drop an element from the domain of one column of the table</param>
        /// <param name="c">Command object corresponding to DropElement statement</param>


        internal override void Expand(DropElement de, Command c)
        {
            if (!(this.tables.Contains(de.Table)))
                return;
            if (this.keyNames[de.Table].Contains(de.Column))
                return;

            int i = this.tables.IndexOf(de.Table);

            List<string> keys = keyNames[de.Table];
            List<string> columns = new List<string>(colNames[de.Table]);
            //get all non key columns in form of a list

            foreach (string key in keys)
            {
                columns.Remove(key);

            }


            /*now the columns list contains only non key columns.In the current transaction
            check if there exists an addelement for each of the element in this columns list
            */
            //get current transaction
            //  List<Command> command= c.container.Contents;
            Transaction t = c.container;
            List<Command> commandlist = t.Contents;
            foreach (Command command in commandlist)
            {
                Operator op = command.root;
                Type type = op.GetType();
                String opname = type.ToString();

                if (opname.Equals("DropElement"))
                {
                    DropElement newre = (DropElement)op;
                    String col = newre.Column;
                    columns.Remove(col);
                    DropElement denew = new DropElement(de.Table, this.valColumns[i], de.Element);
                    c.NotifyChange(c, new Command(denew));

                }
            }

            if (columns != null)
            {
                throw new Exception("Error");
            }

        }


        /// <summary>
        /// expand Insert statement
        /// </summary>
        /// <param name="i">Insert object that insert rows to the table</param>
        /// <param name="c">Command object corresponding to Insert statement</param>

        internal override void Expand(Insert ins, Command c)
        {
            if (!(this.tables.Contains(ins.Table)))
                return;
            List<string> cols = ins.Columns;
            Operator q = ins.Child;
            int i = this.tables.IndexOf(ins.Table);
            ins.Child.Visit(this, c);
            List<string> oldcolumns =  new List<string>(this.colNames[this.tables[i]]); //list of columns of old table
            List<string> oldkeys = this.keyNames[this.tables[i]]; //list of old keys
            
            //create a list of columns to into the new object
            List<string> newcolumns = new List<string>(this.keyNames[ins.Table]); // List of columns for new insert object
            newcolumns.Add(attrColumns[i]);
            newcolumns.Add(valColumns[i]);


            List<Command> newcommands = new List<Command>(); //create a list of commands for notify change
            List<string> columnsToProject = new List<string>(this.keyNames[ins.Table]); //list of coulmns to project
            List<string> orderProjectColumns = new List<string>(this.keyNames[ins.Table]);
            orderProjectColumns.Add(attrColumns[i]);




            if (!(ins.Child is RowConstant))
            {
                foreach (String col in ins.Columns)
                {
                    if (ins.Columns.Contains(col) && !keyNames[ins.Table].Contains(col))
                    {
                        RowConstant rc = new RowConstant();
                        rc.AddValue(attrColumns[i], attrDomains[i], col);
                        Filter f = new Filter(q);
                        f.AddCondition(col, Condition.NewCondition(ConditionType.ISNOTNULL));
                        columnsToProject.Add(col);
                        Project p = new Project(f, new List<string>(columnsToProject));
                        CrossJoin cj = new CrossJoin(p, rc);
                        orderProjectColumns.Add(col);
                        Project p1 = new Project(cj, new List<string>(orderProjectColumns));
                        //cj.exposedColumns = orderProjectColumns;
                        Command com = new Command(new Insert(ins.Table, newcolumns, p1));
                        newcommands.Add(com);
                        orderProjectColumns.Remove(col);
                        columnsToProject.Remove(col);
                    }
                }
            }

            else
            {
                RowConstant rcKeysOnly = new RowConstant();
                RowConstant rcInsChild = ins.Child as RowConstant;
                foreach (string s in oldkeys)
                {
                    for (int j = 0; j < ins.Columns.Count; j++)
                    {
                        if (s == ins.Columns[j])
                        {
                            rcKeysOnly.AddValue(rcInsChild.exposedColumns[j], rcInsChild.exposedDomains[j], rcInsChild.Fields[j]);
                            break;
                        }
                    }
                }
                for (int j = 0; j < ins.Columns.Count; j++)
                {
                    string col = ins.Columns[j];
                    if (oldkeys.Contains(col))
                        continue;
                    RowConstant rc = (RowConstant)rcKeysOnly.Clone();

                    rc.AddValue(attrColumns[i], attrDomains[i], col);
                    rc.AddValue(valColumns[i], valueDomains[i], rcInsChild.Fields[j]);

                    newcommands.Add(new Command(new Insert(ins.Table, newcolumns, rc)));
                }
            }
            c.NotifyChange(newcommands.ToArray());
        }

        /// <summary>
        /// expand Update statement
        /// </summary>
        /// <param name="u">Update object that update some rows of the table</param>
        /// <param name="c">Command object corresponding to Update statement</param>


        internal override void Expand(Update u, Command c)
        {
            if (!(this.tables.Contains(u.Table)))
                return;

            u.Child.Visit(this, c);
            int i = this.tables.IndexOf(u.Table);
            String attr = this.attrColumns[i];
            String val = this.valColumns[i];
            List<string> keycols = new List<string>(this.keyNames[u.Table]);
            List<string> nonkeycolumns = new List<string>(this.colNames[u.Table]);
            List<Command> newcommands = new List<Command>(); //Create a list of commands to pass to notifychange
            // at the end of the function
            List<string> newkeys = new List<string>(this.keyNames[u.Table]);
            List<string> newcols = new List<string>(this.keyNames[u.Table]);
            newkeys.Add(attr);
            newcols.Add(attr);
            newcols.Add(val);

            //get a list of nonkeycolumns
            foreach (String key in keycols)
            {
                if (nonkeycolumns.Contains(key))
                    nonkeycolumns.Remove(key);
            }
            String alias = c.nextTableAlias(u.Table);
            //for each non key column you have to do an insert

            foreach (String col in u.Columns)
            {
                //the query object to be passed to the new insert object is a cross join of
                // an except and a row constant.The excpet in turn consists of 2 projects.
                //the two projects inturn consist of a filter


                //create a query object to pass the insert
                TableRef tr = new TableRef(u.Table, alias);
                for (int j=0; j < colNames[u.Table].Count; j++)
                    tr.AddColumn(colNames[u.Table][j], colNames[u.Table][j], colDomains[u.Table][j]);

                Operator current = tr;
                for (int j = 0; j < u.CondColumns.Count; j++)
                {
                    current = new Filter(current, new string[] { u.CondColumns[j] }, new Condition[] { u.Conditions[j] });
                }

                Operator current2 = current.Clone();                
                Filter newFilterColumnCondition = new Filter(current2,
                    new string[] {attr}, new Condition[] {Condition.NewCondition(ConditionType.EQ,col)});                
                
                Project newproj2 = new Project(current, keycols);
                Project newproj3 = new Project(newFilterColumnCondition, keycols);
                Except expt = new Except(newproj2, newproj3);
                RowConstant rc = new RowConstant();
                rc.AddValue(attr, attrDomains[i], col );
                CrossJoin cj = new CrossJoin(expt, rc);
                Insert newi = new Insert(this.tables[i], newkeys, cj);
                newcommands.Add(new Command(newi));
            }
            // for each non key columns do a project or a delete depending on whether project is a null

            foreach (String col in u.Columns)
            {
                Project p = new Project(u.Child, new string[] { col });
                Condition cond = Condition.NewCondition(ConditionType.EQ, col);
                List<Condition> newconditions = new List<Condition>(u.Conditions);
                List<string> newcondcolumns = new List<string>(u.CondColumns);
                newconditions.Add(cond);
                newcondcolumns.Add(attr);
                
                if (p.exposedColumns != null)
                {
                    
                    Update up = new Update(u.Table, newcondcolumns, newconditions, new string[] { val }, p);
                    if(u.needJoin)
                        foreach(var key in u.idJoin.Keys)
                            up.AddJoin(key,u.idJoin[key]);
                    newcommands.Add(c.CloneState(up));
                }
                else
                {
                    newcommands.Add(new Command(new Delete(u.Table, u.CondColumns, u.Conditions)));
                }
                newconditions.Remove(cond);
                newcondcolumns.Remove(attr);
            }
            c.NotifyChange(newcommands.ToArray());
        }


        /// <summary>
        /// expand ForeignKey statement
        /// </summary>
        /// <param name="fk">ForeignKey object</param>
        /// <param name="c">Command object corresponding to ForeignKey statement</param>

        internal override void Expand(ForeignKey fk, Command c)
        {
            if (!(this.tables.Contains(fk.ChildTable)))
                return;

            // x- child columns
            List<string> keys = new List<string>();
            if (this.keyNames.ContainsKey(fk.ChildTable))
                keys = this.keyNames[fk.ChildTable];
            List<string> cols = new List<string>(fk.ChildColumns);
            int i = this.tables.IndexOf(fk.ChildTable);
            String attr = this.attrColumns[i];
            foreach (String key in keys)
            {
                cols.Remove(key);
            }
            List<Command> clist = new List<Command>();
            if (cols.Count > 0)
            {
                foreach (String col in cols)
                {
                    List<Condition> newchildConditions = new List<Condition>();
                    List<String> newchildConditionColumns = new List<string>();
                    List<String> newparentColumns = new List<string>();
                    List<String> newchildColumns = new List<string>();

                    //child condition 
                    Condition cond = Condition.NewCondition(ConditionType.EQ, col);
                    newchildConditions.AddRange(fk.ChildConditions);
                    newchildConditionColumns.AddRange(fk.ChildConditionColumns);
                    newchildConditions.Add(cond);
                    //child condition column
                    newchildConditionColumns.Add(attr);
                    //newchildConditionColumns.Add(col);
                    //child columns
                    foreach (String tempcol1 in fk.ChildColumns)
                    {
                        if (keys.Contains(tempcol1))
                        {
                            newchildColumns.Add(tempcol1);
                        }
                    }

                    newchildColumns.Add(this.attrColumns[i]);
                    //parents
                    foreach (String tempcol2 in fk.ParentColumns)
                    {
                        if (keys.Contains(tempcol2))
                        {
                            newparentColumns.Add(tempcol2);
                        }
                    }
                    newparentColumns.Add(col);
                    //newparentconditioncolumns
                    ForeignKey fkNew = new ForeignKey(fk.ChildTable, newchildConditionColumns, newchildConditions, newchildColumns,
                            fk.ParentTable, fk.ParentConditionColumns, fk.ParentConditions, newparentColumns);


                    clist.Add(new Command(fkNew));


                }
                c.NotifyChange(clist.ToArray());
            }
        }



        /// <summary>
        /// expand TableRef statement
        /// </summary>
        /// <param name="tr">TableRef object that reference the table</param>
        /// <param name="c">Command object corresponding to TableRef statement</param>

        internal override void Expand(TableRef tr, Command c)
        {
            if (!(this.tables.Contains(tr.Table)))
                return;

            int j = this.tables.IndexOf(tr.Table);

            string t = this.tables[j];
            string a = this.attrColumns[j];
            string v = this.valColumns[j];


            List<string> keys = new List<string>(this.keyNames[t]);
            List<string> cols = new List<string>(this.colNames[t]);
            foreach (String key in keys)
            {
                cols.Remove(key);
            }

            string talias = tr.Alias;
            TableRef tref = new TableRef(tr.Table, talias);
            var aliasMap = new Dictionary<string, string>();
            var keyMap = new Dictionary<string, string>();

            for (int i = 0; i < tr.Columns.Count; i++)
            {
                if (keys.Contains(tr.Columns[i]))
                {
                    tref.AddColumn(tr.Columns[i], tr.Columns[i], tr.exposedDomains[i]);
                    keyMap.Add(tr.Columns[i], tr.exposedColumns[i]);
                }
                else
                    aliasMap.Add(tr.Columns[i], tr.exposedColumns[i]);
            }
            if (aliasMap.Count == 0)
            {

                for (int i = 0; i < this.colNames[t].Count; i++)
                {
                    if (!keys.Contains(this.colNames[t][i]))
                    {
                        aliasMap.Add(this.colNames[t][i], this.colNames[t][i]);
                    }
                }
            }

            tref.AddColumn(a, a, attrDomains[j]);
            tref.AddColumn(v, v, valueDomains[j]);

            PivotOp pivot = new PivotOp(tref, keyMap, aliasMap, a, v, talias);

            if (tr.parent != null)
                tr.parent.NotifyChange(tr, pivot);
        }
        public override GuavaData.Transform Inverse()
        {
            return new Pivot(tables, attrColumns, valColumns);
        }
        public override List<XElement> ToXElement()
        {
            XElement x = new XElement("Transform", new XAttribute("type", "Unpivot"));
            x.Add(new XElement("tables"));
            x.Add(new XElement("attributes"));
            x.Add(new XElement("values"));
            for (int i = 0; i < tables.Count; i++)
            {
                x.Element("tables").Add(new XElement(tables[i]));
                x.Element("attributes").Add(new XElement(attrColumns[i]));
                x.Element("values").Add(new XElement(valColumns[i]));
            }
            List<XElement> xList = new List<XElement>();
            if (!(this.Child is SqlProvider))
                xList = ((Transform)this.Child).ToXElement();
            xList.Add(x);
            return xList;
        }
        new public static Unpivot FromXElement(XElement x)
        {
            List<string> tables = new List<string>();
            List<string> attributes = new List<string>();
            List<string> values = new List<string>();
            List<XElement> xTables = new List<XElement>();
            List<XElement> xAttributes = new List<XElement>();
            List<XElement> xValues = new List<XElement>();
            foreach (var xe in x.Elements())
            {
                List<XElement> elist = new List<XElement>();
                switch (xe.Name.LocalName)
                {
                    case "tables":
                        
                        foreach (var el in xe.Elements())
                        {
                            elist.Add(el);
                        }
                        xTables = elist;
                        //xTables = (List<XElement>)xe.Elements();
                        break;
                    case "attributes":
                        foreach (var el in xe.Elements())
                        {
                            elist.Add(el);
                        }
                        xAttributes = elist;
                        //xAttributes = (List<XElement>) xe.Elements();
                        break;
                    case "values":
                        foreach (var el in xe.Elements())
                        {
                            elist.Add(el);
                        }
                        xValues = elist;
                        //xValues = (List<XElement>) xe.Elements();
                        break;
                    default:
                        throw new Exception("Unpivot.FromXElement: Unexpected elements found in XML");
                }
            }
            for (int i = 0; i < xTables.Count(); i++)
            {
                tables.Add(xTables[i].Name.LocalName);
                attributes.Add(xAttributes[i].Name.LocalName);
                values.Add(xValues[i].Name.LocalName);
            }

            return new Unpivot(tables,attributes, values);
        }

    }
}
